﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Red.Core.Extensions;
using Red.Launcher.Utilities;

namespace Red.Launcher.Diagnostics.Process
{
    public class Monitor
    {
        #region Private Types

        private class Compare : IEqualityComparer<System.Diagnostics.Process>
        {
            public bool Equals( System.Diagnostics.Process x, System.Diagnostics.Process y )
            {
                return x.Id == y.Id;
            }

            public int GetHashCode( System.Diagnostics.Process obj )
            {
                return obj.Id.GetHashCode();
            }
        }

        #endregion Private Types

        #region Public Properties

        public Definition Definition { get; set; }
        public ObservableCollection<System.Diagnostics.Process> Primary { get; private set; } = new ObservableCollection<System.Diagnostics.Process>();
        public ObservableCollection<System.Diagnostics.Process> Children { get; private set; } = new ObservableCollection<System.Diagnostics.Process>();
        public int Count => Primary.Count + Children.Count;

        #endregion Public Properties

        #region C-Tor

        public Monitor()
        {
            Primary.CollectionChanged += Primary_CollectionChanged;
            Children.CollectionChanged += Primary_CollectionChanged;
        }

        #endregion C-Tor

        #region Public Methods

        public async Task Refresh()
        {
            IEnumerable<System.Diagnostics.Process> toAdd = await Task.Run( () => { return Fetch(); } );

            Add( toAdd );
        }

        #endregion Public Methods

        #region Private Methods

        private IEnumerable<System.Diagnostics.Process> Fetch()
        {
            System.Diagnostics.Process[] processes = System.Diagnostics.Process.GetProcesses();
            IEnumerable<System.Diagnostics.Process> fetched = processes.Where( Filter );

            // ToList() will force the filter to be executed now (rather than lazily executed later)
            return fetched.ToList();
        }

        private void Add( IEnumerable<System.Diagnostics.Process> processes )
        {
            Compare exceptCompare = new Compare();

            var allProcesses = Primary.Union( Children );
            IEnumerable<System.Diagnostics.Process> newProcesses = processes.Except( allProcesses, exceptCompare );

            foreach ( System.Diagnostics.Process process in newProcesses )
            {
                Primary.Add( process );

                if ( Definition.IncludeChildren )
                {
                    Children.AddRange( process.GetChildProcesses().Except( allProcesses, exceptCompare ) );
                }

                Debug.Print( $"Process monitor for '{Definition.Name}' now monitoring '{process.ProcessName}'" );
            }
        }

        private bool Filter( System.Diagnostics.Process process )
        {
            return FilterCollection( Definition, process );
        }

        private static bool FilterCollection( Definition definition, System.Diagnostics.Process process )
        {
            if ( FilterDefinition( definition, process ) )
                return true;

            foreach ( Definition collected in definition.Collection )
            {
                if ( FilterDefinition( collected, process ) )
                    return true;
            }

            return false;
        }

        private static bool FilterDefinition( Definition definition, System.Diagnostics.Process process )
        {
            // Have we found a matching application?
            if ( process.ProcessName == definition.Name )
            {
                // First check it against it's parent list
                if ( definition.Parents.Count > 0 )
                {
                    System.Diagnostics.Process parentProcess = process.GetParentProcess();

                    if ( parentProcess != null )
                    {
                        foreach ( Definition parentDefinition in definition.Parents )
                        {
                            if ( parentProcess.ProcessName == parentDefinition.Name )
                                return !parentDefinition.Ignore;
                        }
                    }
                }

                // No parent or parents didn't match, so return if it's a successful match at this level
                return !definition.Ignore;
            }

            return false;
        }

        #endregion Private Methods

        #region Event Handlers

        private void Primary_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
        {
            if ( e.Action == NotifyCollectionChangedAction.Add )
            {
                foreach ( System.Diagnostics.Process process in e.NewItems )
                {
                    process.EnableRaisingEvents = true;
                    process.Exited += Process_Exited;
                }
            }
        }

        private void Process_Exited( object sender, EventArgs e )
        {
            System.Diagnostics.Process process = (System.Diagnostics.Process)sender;

            process.Exited -= Process_Exited;

            Primary.Remove( process );
            Children.Remove( process );
        }

        #endregion Event Handlers
    }
}
